docs(chat-store): document per-state receipt rows for 1:1 chats (#1155) - #455
Conversation
`receipts()` now returns one row per participant per delivery/read/played state instead of only the furthest state, and only for group chats. Update the querying, companion-device, PN/LID merge, and semantics sections to match, plus a breaking-change note for callers assuming at most one row per participant.
📝 WalkthroughWalkthroughUpdates ChangesReceipt semantics
Estimated code review effort: 3 (Moderate) | ~15–30 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@api/chat-store.mdx`:
- Line 147: Rewrite the newly added documentation at the referenced sections in
active, second-person voice: address the reader as “you,” replace impersonal or
third-person phrasing and passive constructions, and split long multi-rule
sentences into concise one-idea sentences while preserving the documented
behavior and terminology.
- Line 459: Revise the receipt semantics wording in the “Monotonic status,
insert-only receipts” documentation to clarify that state membership is
insert-only: existing Delivered, Read, or Played rows are never removed, while
their timestamps may be updated only when a newly reported timestamp is earlier.
Remove the conflicting claim that existing rows are never overwritten.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: b2548c92-b72b-42c1-be92-ee87cd021f30
📒 Files selected for processing (1)
api/chat-store.mdx
| - **A brand-new chat is keyed by LID** when the peer already has a known PN↔LID mapping; otherwise it's keyed by whichever identity first addressed it. | ||
| - **Every read resolves the alias.** [`messages`](#querying), [`message`](#querying), [`reactions`](#querying) and [`receipts`](#querying) all accept either of the peer's identities as the `chat` argument and match rows stored under either key — so a caller that only ever addresses a peer by phone number keeps working even if some rows ended up under the LID key (or vice versa). | ||
| - **Splits heal automatically.** If a peer's rows are split across both keys (for example, from receipts that arrived under the wrong identity before this resolution existed), the next piece of live traffic for that peer merges the pair into one thread — advance-only status/star/revoke/edit conflict resolution, union of reactions and per-user receipts, sticky metadata (pin/mute/archive/name/ephemeral) kept, badge recounted. Ties go to the LID side. | ||
| - **Splits heal automatically.** If a peer's rows are split across both keys (for example, from receipts that arrived under the wrong identity before this resolution existed), the next piece of live traffic for that peer merges the pair into one thread — advance-only status/star/revoke/edit conflict resolution, union of reactions and per-state receipts (a state both sides already recorded keeps the earlier of the two timestamps, rather than either side winning arbitrarily), sticky metadata (pin/mute/archive/name/ephemeral) kept, badge recounted. Ties go to the LID side. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Rewrite the new prose in active, second-person, concise style.
These changes repeatedly use impersonal or third-person phrasing such as “a caller,” “a message,” and “a group member,” plus passive constructions such as “are recorded” and “was reported.” Rewrite directly for the reader—for example, “When you call receipts(), you receive…”—and split the long multi-rule sentences into one idea per sentence.
As per coding guidelines, MDX documentation must use active voice and second person, with concise one-idea sentences.
Also applies to: 162-162, 213-217, 400-401, 459-459
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@api/chat-store.mdx` at line 147, Rewrite the newly added documentation at the
referenced sections in active, second-person voice: address the reader as “you,”
replace impersonal or third-person phrasing and passive constructions, and split
long multi-rule sentences into concise one-idea sentences while preserving the
documented behavior and terminology.
Source: Coding guidelines
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 439c4a1ea7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ## Semantics worth knowing | ||
|
|
||
| - **Monotonic status.** Per-message status and per-user group receipts only ever move forward; a late-arriving `delivered` can't downgrade a `read`. | ||
| - **Monotonic status, insert-only receipts.** `StoredMessage.status` only ever moves forward — a late-arriving `delivered` can't downgrade a `read`. [`ReceiptEntry`](#reactionentry--receiptentry) rows are insert-only per state instead: a state (`Delivered`/`Read`/`Played`) is recorded once, at the earliest instant it was reported, and a replay or an out-of-order receipt for a state already recorded never removes or overwrites that row — it only lowers the timestamp if the same state is reported as having happened earlier. |
There was a problem hiding this comment.
Stop calling mutable receipt rows insert-only
When a duplicate receipt for the same state arrives with an earlier timestamp, this sentence says the stored timestamp is lowered. That updates the existing participant/state row, so calling the rows “insert-only” and saying they are never overwritten gives readers the opposite persistence guarantee. Describe this as one mutable row per state, or as an upsert that retains the minimum timestamp.
Useful? React with 👍 / 👎.
| - **Offline-drain reordering is handled.** If a revoke or edit arrives before the message it targets (common when draining a backlog), the target is materialized as already-revoked/edited up front — the original content's later arrival can't resurrect revoked content or show pre-edit text, and never double-counts unread. | ||
| - **History sync never clobbers live rows** (`ON CONFLICT DO NOTHING`). Live redeliveries and PDO recovery replace content in place instead — an `Undecryptable` placeholder becomes the real message under the same id once it's recovered. | ||
| - **Content refreshes are sender-scoped.** Message ids are sender-chosen, so a different sender reusing an id can't rewrite someone else's message. | ||
| - **A receipt for a message no chat holds is dropped, not parked.** The message id is the server's, and nothing in the store can tell an unrecorded send from a message the user already deleted — the second reading is the common one, since a peer's receipt costs a round trip and typically arrives well after the send it answers. A receipt is only ever recorded once the message it names is found under the addressed chat key or its PN/LID counterpart. |
There was a problem hiding this comment.
Correct the message ID ownership
Message IDs are sender-chosen, not assigned by the server; the immediately preceding bullet documents this, and record_outgoing accepts the caller's generated msg_id. Calling the ID “the server's” is therefore factually incorrect and obscures the actual reason an unmatched receipt is ambiguous: the store has only the receipt's chat/ID lookup key and no row indicating whether that locally chosen ID was never recorded or was deleted.
Useful? React with 👍 / 👎.
| ``` | ||
|
|
||
| `message`, `reactions` and `receipts` accept either of a 1:1 peer's identities the same way `messages` does. `receipts` returns per-user delivery/read state — the group "read by" list. `unread_total` sums only positive unread counters, ignoring the `-1` manually-marked-unread sentinel on individual chats. | ||
| `message`, `reactions` and `receipts` accept either of a 1:1 peer's identities the same way `messages` does. `receipts` returns one row per participant *per state they've reached* — `Delivered`, `Read`, and `Played` are recorded as separate rows, each carrying the instant that state was first reported — rather than one row per participant holding only the furthest state. `unread_total` sums only positive unread counters, ignoring the `-1` manually-marked-unread sentinel on individual chats. |
There was a problem hiding this comment.
Split the receipt contract into one-idea sentences
The new receipts sentence combines row cardinality, the supported states, timestamp semantics, and a comparison with the previous representation. This makes the API contract unnecessarily difficult to scan; split those independent points into concise sentences as required by the repository's documentation style.
AGENTS.md reference: AGENTS.md:L24-L25
Useful? React with 👍 / 👎.
"Never removes or overwrites that row" conflicted with the following clause allowing the timestamp to be lowered. State the rule as: the row is never removed, and its timestamp may only move down to an earlier reported instant. Addresses a CodeRabbit review comment on #455.
- Fix a factual error: a receipt's message id is sender-chosen and echoed back by the peer, not server-assigned — this directly contradicted the "Message ids are sender-chosen" bullet one line above it. - Reword the receipt-timestamp mutability rule to state plainly that a state row is never deleted but its timestamp can move down to an earlier reported instant, dropping the ambiguous "insert-only" framing. - Split several multi-clause sentences added in the prior commits into one idea per sentence, per AGENTS.md's documentation style guidance. Addresses coderabbitai and chatgpt-codex-connector review comments on #455.
|
Pushed fixes for the review feedback so far, in
Not doing: CodeRabbit's broader ask to rewrite these sections in second-person ("you") voice. That guideline is real ( Generated by Claude Code |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8f9dec3b86
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| `message`, `reactions` and `receipts` accept either of a 1:1 peer's identities the same way `messages` does. `receipts` returns one row per participant per state they've reached, not one row per participant. `Delivered`, `Read`, and `Played` are each recorded as a separate row. Each row's `timestamp` is the instant that state was first reported. `unread_total` sums only positive unread counters, ignoring the `-1` manually-marked-unread sentinel on individual chats. | ||
|
|
||
| <Note> | ||
| **Breaking change:** `receipts` used to return rows only for group chats. A 1:1's delivery/read state lived solely in `StoredMessage.status`, with no per-state timestamp available. It now records the same per-state rows for 1:1 chats too, so a caller can render "Delivered hh:mm" above "Read hh:mm" the way WA Web's contact message-info drawer does. A caller that assumed at most one row per participant should now expect up to three (`Delivered`, `Read`, `Played`), oldest state first. |
There was a problem hiding this comment.
Split the migration guidance into one-idea sentences
The final sentence combines the caller's migration condition, the new maximum row count, the supported states, and the result ordering guarantee. Split the cardinality/state guidance from the oldest state first guarantee so readers can scan each contract change independently.
AGENTS.md reference: AGENTS.md:L24-L25
Useful? React with 👍 / 👎.
| ``` | ||
|
|
||
| `message`, `reactions` and `receipts` accept either of a 1:1 peer's identities the same way `messages` does. `receipts` returns per-user delivery/read state — the group "read by" list. `unread_total` sums only positive unread counters, ignoring the `-1` manually-marked-unread sentinel on individual chats. | ||
| `message`, `reactions` and `receipts` accept either of a 1:1 peer's identities the same way `messages` does. `receipts` returns one row per participant per state they've reached, not one row per participant. `Delivered`, `Read`, and `Played` are each recorded as a separate row. Each row's `timestamp` is the instant that state was first reported. `unread_total` sums only positive unread counters, ignoring the `-1` manually-marked-unread sentinel on individual chats. |
There was a problem hiding this comment.
Describe rows as reported states, not reached states
When the store observes a Read receipt without having observed Delivered first, it records the reported Read state rather than synthesizing a row for every logically reached state. Saying receipts returns a row for each state the participant has reached can therefore make callers assume a Delivered row is guaranteed whenever a Read row exists; describe these as the states whose receipts were actually reported.
Useful? React with 👍 / 👎.
…d state record_receipt only ever inserts a row for the exact status an incoming receipt carries — it never backfills earlier states a peer skipped (e.g. a Read receipt with no prior Delivered receipt produces only a Read row). "Reached" implied logical progression through all prior states; reworded throughout to "reported" and called out the no-backfill case explicitly on ReceiptEntry. Also split the receipts() breaking-change note's closing sentence (cardinality + states + ordering) into two. Addresses chatgpt-codex-connector review comments on #455.
|
Fixed both in
Generated by Claude Code |
Summary
Documents the user-facing behavior change from whatsapp-rust#1155 ("fix(chat-store): keep receipt times for 1:1 chats, per state").
apply_receiptused to materializemessage_receiptsrows only for group chats, one row per participant holding the furthest state reached. A 1:1's delivery/read state lived solely inStoredMessage.status, with no per-state timestamp — so a UI could show "Delivered" but never "Delivered 14:32". The upstream PR changesreceipts()to record one row per state (Delivered/Read/Played) per participant, for both 1:1 and group chats, each row keeping the earliest instant that state was reported.Updated
api/chat-store.mdx:receipts()description to say it returns one row per participant per state, not per participant, and added a<Note>calling out the breaking change (1:1 chats previously returned no rows at all; callers assuming at most one row per participant should now expect up to three).ReactionEntry/ReceiptEntry— added a paragraph explaining a message can carry severalReceiptEntryrows peruser_jid, one per state.StoredMessage.status(monotonic) vs. receipt rows (insert-only per state) separately, and added a bullet on receipts for messages no chat holds being dropped rather than parked (an existing but previously undocumented behavior this PR's tests also cover).Not documented: the internal SQL merge mechanics in
storages/chat-store/src/lid.rs(fold-then-rename-then-sweep passes) or the migration itself — no observable API surface beyond what's covered above. Per standing instructions,changelog/(human-maintained) was left untouched.Test plan
dm_receipts_record_when_each_state_was_reached,dm_played_receipt_joins_read_rather_than_replacing_it,a_replayed_dm_receipt_does_not_move_the_recorded_instant,a_dm_receipt_resolved_by_alias_files_under_the_message_key,group_receipts_from_two_devices_keep_one_participant) for accuracymessage_receipts/receipts()semantics that would also need updating (searched full repo)mint broken-links(not run — CLI unavailable in this environment)Generated by Claude Code
Summary by cubic
Document per-state receipt rows for 1:1 chats, captured per reported state with no backfill, and clarify how receipt timestamps behave. Also fix receipt message-id ownership and tighten wording.
Migration
ReceiptEntryrows per participant; rows are per reported state (Delivered/Read/Played) with no backfill;receipts()now returns rows for 1:1 chats.timestampmay only move earlier.Bug Fixes
Written for commit abd75ee. Summary will update on new commits.
Summary by CodeRabbit
receipts()result format.